export const noop = () => {
  // nothing
}

export const isPlainObject = (o: any): o is object => {
  return Object.prototype.toString.call(o) === '[object Object]'
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const IDENTICAL = <T = any, R = any>(e: T): R => e as unknown as R

export function mapOf<T, R = T>(
  list: Readonly<T[]>,
  keyFn: (e: T, i: number, a: Readonly<T[]>) => string | number,
  valueFn: (e: T, i: number, a: Readonly<T[]>) => R = IDENTICAL
) {
  const map: { [key: string]: R } = {}
  list.forEach((e, i, a) => {
    map[keyFn(e, i, a)] = valueFn(e, i, a)
  })
  return map
}

export function distinct<T>(
  keyFn: (v: T, i: number, a: T[]) => any = IDENTICAL
) {
  const map = {} as { [key: string]: boolean }
  return (v: T, i: number, a: T[]) => {
    const key = keyFn(v, i, a)
    if (map[key as string]) return false
    map[key as string] = true
    return true
  }
}

export function buildTree<T, CK extends string>(
  list: Readonly<T[]>,
  idKey: keyof T = 'id' as keyof T,
  pidKey: keyof T = 'parentId' as keyof T,
  childrenKey: CK = 'children' as CK,
  parentKey?: keyof T
) {
  const map = mapOf<T, T>(list, (e) => e[idKey] as any)
  const root: (T & { [key in CK]?: T[] })[] = []
  list.forEach((e: any) => {
    const parent = (e[pidKey] ? map[e[pidKey] as any] : null) as any
    if (!parent) {
      root.push(e as any)
      return
    }
    if (!parent[childrenKey as any]) parent[childrenKey as any] = []
    ;(parent[childrenKey as any] as T[]).push(e)
    if (parentKey) {
      e[parentKey as any] = parent
    }
  })
  return root
}

/**
 * 当 visitTree 的 visit 返回该值时，停止遍历该节点及其子树（仅 !deepFirst 时有效）
 */
export const VISIT_CONTINUE = Object.freeze({})
/**
 * 当 visitTree 的 visit 返回该值时，在遍历完该节点及其子树后，停止（仅 !deepFirst 时有效）
 */
export const VISIT_BREAK = Object.freeze({})

export type TreeNodeVisitor<T, RT> = (
  node: T,
  index: number,
  parents: T[],
  depth: number,
  siblings: T[]
) => RT | undefined | typeof VISIT_CONTINUE | typeof VISIT_BREAK

export function visitTree<T, CK extends string, RT>(
  list: T[],
  visit: TreeNodeVisitor<T, RT>,
  deepFirst?: boolean,
  childrenKey: CK = 'children' as CK,
  parents: T[] = [],
  depth = 0
) {
  let ret: any
  for (let i = 0; i < list.length; i++) {
    let stopFlag = false
    const e = list[i] as any
    if (!deepFirst) ret = visit(e, i, parents, depth, list)
    if (ret === VISIT_CONTINUE) continue
    if (ret === VISIT_BREAK) {
      ret = undefined
      stopFlag = true
    }
    if (ret !== undefined) break
    if (e[childrenKey as string]?.length > 0) {
      const parents_ = [...parents, e]
      ret = visitTree(
        e[childrenKey as string],
        visit,
        deepFirst,
        childrenKey,
        parents_,
        depth + 1
      )
      if (ret !== undefined) break
    }
    if (stopFlag) break
    if (deepFirst) ret = visit(e, i, parents, depth, list)
    if (ret !== undefined) break
  }
  if (ret === VISIT_CONTINUE) return undefined
  return ret as RT | undefined
}

export const eatError = (e: any) => {
  if (typeof e?.then === 'function') {
    return e.catch(noop)
  }
  return e
}

export interface CacheMapItem<RT> {
  cached?: boolean
  result?: RT
  promise?: any
}

/** 为函数添加缓存，返回的 fn 为目标函数的包装，evict 为清除缓存方法*/
export function cacheFn<
  T extends (this: THIS, ...args: ARGS) => RT,
  THIS,
  ARGS extends any[] = any[],
  RT = any
>(
  fn: T,
  argsSerialize: (this: THIS, ...args: ARGS) => any
): {
  fn: T
  evict: (key?: string) => void
  cache: Record<string, CacheMapItem<RT>>
} {
  const cacheMap = {} as Record<string, CacheMapItem<RT>>
  let isPromise: boolean
  const evictCache = (key: any) => {
    if (!argsSerialize) key = '_root_'
    if (key === undefined) {
      Object.keys(cacheMap).forEach((key) => {
        delete cacheMap[key]
      })
    } else delete cacheMap[key]
  }
  const cachedFn = function (...args: ARGS) {
    const key = argsSerialize ? argsSerialize.apply(this, args) : '_root_'
    let m = cacheMap[key]
    if (!m) {
      cacheMap[key] = m = {
        cached: undefined,
        result: undefined,
        promise: undefined,
      }
    }

    if (m.cached === true) {
      return (isPromise ? Promise.resolve(m.result) : m.result) as RT
    } else if (m.cached === false) {
      return m.promise.promise as RT
    }

    const r = fn.apply(this, args)
    if (r && typeof (r as any).then === 'function') {
      isPromise = true
      m.cached = false

      m.promise = {}
      m.promise.promise = new Promise((resolve, reject) => {
        m.promise.resolve = resolve
        m.promise.reject = reject
      })
      ;(r as any).then(
        (rr: RT) => {
          m.cached = true
          m.result = rr
          m.promise.resolve(rr)
          m.promise = undefined
        },
        (e: any) => {
          m.cached = undefined
          m.promise.reject(e)
        }
      )

      return m.promise.promise as RT
    } else {
      m.result = r
      m.cached = true
      return r
    }
  } as T
  return {
    fn: cachedFn,
    evict: evictCache,
    cache: cacheMap,
  }
}

export function waitPromise<
  T extends (this: THIS) => RT,
  THIS,
  RT extends Promise<any> = Promise<any>
>(fn: T): T {
  let promise: Promise<any> | null
  return function () {
    if (promise) return promise
    promise = fn.call(this as any).finally(() => {
      promise = null
    })
    return promise
  } as T
}

const propsArrayMap: Record<string, string[]> = {}

export function getProp<T extends Record<string, any> = Record<string, any>>(
  data: T | undefined,
  prop?: string | number | ((d: T) => any)
) {
  if (!data) return
  if (typeof prop === 'function') return prop(data)
  if (!prop || typeof prop !== 'string') return
  let a = propsArrayMap[prop]
  if (!a) {
    a = propsArrayMap[prop] = prop.split('.')
  }
  return a.reduce((a, p) => a && a[p], data)
}

export function setProp<T extends Record<string, any> = Record<string, any>>(
  obj: T,
  prop: string,
  value: any
) {
  let pa = propsArrayMap[prop]
  if (!pa) {
    propsArrayMap[prop] = pa = prop.split('.')
  }
  let v: any = obj
  if (pa.length > 1) {
    for (let i = 0; i < pa.length - 1; i++) {
      if (!v[pa[i]]) {
        v[pa[i]] = {}
      }
      v = v[pa[i]]
    }
  }
  v[pa[pa.length - 1]] = value
  return obj
}
